Skip to content

GitHub Issue #1130: Metrics to track R & Python package usage - #7881

Open
cnathe wants to merge 15 commits into
developfrom
fb_pacakgeUsage1130
Open

GitHub Issue #1130: Metrics to track R & Python package usage#7881
cnathe wants to merge 15 commits into
developfrom
fb_pacakgeUsage1130

Conversation

@cnathe

@cnathe cnathe commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Rationale

https://github.com/LabKey/internal-issues/issues/1130

The change instruments ExternalScriptEngine to record which R packages / Python modules scripts load, so the counts get reported to mothership via SimpleMetricsService. It does this by: (1) appending a language-specific "capture epilog" to the end of the user script that writes loaded packages to a sidecar file; (2) reading that sidecar back after the run and incrementing a per-package counter; and (3) adding a PythonScriptEngine subclass plus manager wiring to detect .py engines. All metric work is wrapped so it can never break script execution.

Related Pull Requests

Changes

  • ExternalScriptEngine refactor to allow for getPackageCaptureEpilog, recordPackageUsage, and recordSuccessfulRun overrides
  • RScriptEngine implementations for tracking package usages and recording via ScriptPackageUsageTracker.record()
  • PythonScriptEngine implementations for tracking package usages and recording via ScriptPackageUsageTracker.record()
  • ScriptPackageUsageTracker to use SimpleMetricsService to increment count of number of times a package was used

@cnathe cnathe self-assigned this Jul 24, 2026
@cnathe

cnathe commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

@labkey-jeckels @labkey-matthewb note that this approach doesn't track anything for Jupyter reports or those that run via Rserve. Thoughts?

@labkey-jeckels labkey-jeckels left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not worried about Jupyter since we only have reports on our own cloud servers, not customer instances.

https://www.labkey.org/_mothership/wiki-page.view?name=adhocReport&ids=32119&serverType=customers

See what you think about the suggestions to inject as a shutdown-type handler.

Comment thread api/src/org/labkey/api/reports/ExternalScriptEngine.java
if (extensions.isEmpty())
throw new ScriptException("There are no file name extensions registered for this ScriptEngine : " + getFactory().getLanguageName());

String epilog = getPackageCaptureEpilog(context);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't dive deep to fully prove it to myself, but Claude thinks that RserveScriptEngine won't capture the package lists.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct. I called this out in the PR comment. Claude does seem to think it wouldn't be too large of a change to get this behavior for RserverScriptEngine, so I'm looking into that now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to add the RserveScriptEngine implementation to match. It was overriding the entire eval() method so just needed to poke in the calls to these new methods.

* (via sys.stdlib_module_names), so no Python entry is needed.
*/
private static final Map<String, Set<String>> BASE_PACKAGES = Map.of(
"r", Set.of("base", "compiler", "datasets", "graphics", "grDevices", "methods", "stats", "utils")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude thinks that these are missing from the list: grid, tools, parallel, splines, stats4, and tcltk

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add "tools" to the list, but I don't get any of the other ones listed when I open Rstudio locally and run sort(loadedNamespaces()) or when I open a new R report on nightly and run that same comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, I ended up adding that full set to the BASE_PACKAGES list

@cnathe
cnathe requested a review from labkey-jeckels July 28, 2026 18:23
def _lk_write_packages():
try:
_lk_stdlib = set(getattr(_lk_sys, 'stdlib_module_names', ()))
_lk_mods = sorted({m.split('.')[0] for m in list(_lk_sys.modules)} - _lk_stdlib)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe this will give us the most useful output. In the Python ecosystem you can have a package that's installed like pip install lib_foo but imported as import foo, I believe the code here will only ever report foo, when what we really want is lib_foo. We should be able to use the importlib.metadata packages_distributions() mapping to convert the imported names to the library names. See here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changes made

@labkey-jeckels labkey-jeckels left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing any automated test coverage yet. It should be easy to inject that into an existing test that runs scripts.

{
if (recorded >= MAX_PACKAGES_PER_RUN)
{
LOG.warn("Recorded the first {} {} packages for this script run and ignored the rest", MAX_PACKAGES_PER_RUN, language);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good warning to log. We could also inject a special packageName like ~~packageLimitReached~~ here that would should up in metrics.

_lk_dists = _lk_md.packages_distributions()
_lk_tops = {m.split('.')[0] for m in list(_lk_sys.modules)} - set(_lk_sys.stdlib_module_names)
_lk_names = sorted({d for t in _lk_tops if t and not t.startswith('_') for d in _lk_dists.get(t, ())})
with open(_lk_os.path.join(_lk_os.getcwd(), '%s'), 'w') as _lk_f:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes that the script hasn't changed the working directory. Maybe we should embed the path directly?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants